/**
 * mds — A micro-display server
 * Copyright © 2014, 2015  Mattias Andrée (maandree@member.fsf.org)
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include "address.h"

#include <stddef.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netdb.h>
#include <netinet/ip.h>
#include <limits.h>
#include <string.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <alloca.h>

#include <libmdsserver/config.h>



/**
 * Check whether a string is a radix-10 non-negative integer
 * 
 * @param   str  The string
 * @return       Whether a string is a radix-10 non-negative integer
 */
__attribute__((nonnull))
static int is_pzinteger(const char* restrict str)
{
  for (; *str; str++)
    if (('0' > *str) || (*str > '9'))
      return 0;
  return 1;
}


/**
 * Set the socket address, with the address family `AF_UNIX`
 * 
 * @param   out_address  Output parameter for the socket address
 * @param   pathlen      Pointer to a variable where the length of the pathname will be stored
 * @param   format       Formatting string for the pathname (should end with "%zn")
 * @param   ...          Formatting arguments for the pathname (should end with `pathlen`)
 * @return               Zero on success, -1 on error, `errno`
 *                       will have been set accordinly on error
 * 
 * @throws  ENOMEM        Out of memory. Possibly, the application hit the
 *                        RLIMIT_AS or RLIMIT_DATA limit described in getrlimit(2).
 * @throws  ENAMETOOLONG  The filename of the target socket is too long
 */
__attribute__((nonnull, format(gnu_printf, 3, 4)))
static int set_af_unix(struct sockaddr** restrict out_address, ssize_t* pathlen, const char* format, ...)
#define set_af_unix(a, f, ...)  ((set_af_unix)(a, &pathlen, f "%zn", __VA_ARGS__, &pathlen))
{
  struct sockaddr_un* address;
  size_t maxlen;
  va_list args;
  
  address = malloc(sizeof(struct sockaddr_un));
  *out_address = (struct sockaddr*)address;
  if (address == NULL)
    return -1;
  
  maxlen = sizeof(address->sun_path) / sizeof(address->sun_path[0]);
  
  va_start(args, format);
  
  address->sun_family = AF_UNIX;
  vsnprintf(address->sun_path, maxlen, format, args);
  
  va_end(args);
  
  if ((size_t)*pathlen > maxlen)
    return errno = ENAMETOOLONG, -1;
  
  return 0;
}


/**
 * Set the socket address, with either of the address
 * families `AF_INET` and `AF_INET6`
 * 
 * @param   out_address      Output parameter for the socket address
 * @param   out_address_len  Output parameter for the socket address's allocation size
 * @param   out_gai_error    Output parameter for error at `getaddrinfo`
 * @param   out_domain       Output parameter for the protocol famility,
 *                           unused unless the address familiy is unknown
 * @param   address_family   The address family, `AF_UNSPEC` if unknown
 * @param   host             The host
 * @param   port             The address
 * @return                   Zero on success, -1 on error, `errno` will have been
 *                           set accordinly on error, `*out_gai_error` may be set
 *                           on error and zero returned
 * 
 * @throws  ENOMEM  Out of memory. Possibly, the application hit the
 *                  RLIMIT_AS or RLIMIT_DATA limit described in getrlimit(2).
 */
__attribute__((nonnull))
static int set_af_inet(struct sockaddr** restrict out_address, socklen_t* restrict out_address_len,
		       int* restrict out_gai_error, int* restrict out_domain, int address_family,
		       const char* restrict host, const char* restrict port)
{
  struct addrinfo hints;
  struct addrinfo* result;
  int saved_errno;
  
  memset(&hints, 0, sizeof(hints));
  hints.ai_family = address_family;
  
  *out_gai_error = getaddrinfo(host, port, &hints, &result);
  if (*out_gai_error)
    return 0;
  
  *out_address = malloc(result->ai_addrlen);
  if (*out_address == NULL)
    return saved_errno = errno, freeaddrinfo(result), errno = saved_errno, -1;
  memcpy(*out_address, result->ai_addr, result->ai_addrlen);
  
  *out_address_len = result->ai_addrlen;
  *out_domain =
    result->ai_family == AF_UNSPEC ? PF_UNSPEC :
    result->ai_family == AF_INET   ? PF_INET   :
    result->ai_family == AF_INET6  ? PF_INET6  :
    result->ai_family;
  
  freeaddrinfo(result);
  return 0;
}


/**
 * Parse a display address string
 * 
 * @param   display  The address in MDS_DISPLAY-formatting, must not be `NULL`
 * @param   address  Output parameter for parsed address, must not be `NULL`
 * @return           Zero on success, even if parsing failed, -1 on error,
 *                   `errno` will have been set accordinly on error
 * 
 * @throws  ENOMEM        Out of memory. Possibly, the application hit the
 *                        RLIMIT_AS or RLIMIT_DATA limit described in getrlimit(2).
 * @throws  ENAMETOOLONG  The filename of the target socket is too long
 */
int libmds_parse_display_address(const char* restrict display, libmds_display_address_t* restrict address)
{
  ssize_t pathlen = 0;
  char* host;
  char* port;
  char* params;
  size_t i = 0;
  char c;
  int esc = 0;
  int colesc = 0;
  
  address->domain = -1;
  address->type = -1;
  address->protocol = -1;
  address->address = NULL;
  address->address_len = 0;
  address->gai_error = 0;
  
  if (strchr(display, ':') == NULL)
    return 0;
  
  if (*display == ':')
    {
      address->domain = PF_UNIX;
      address->type = SOCK_STREAM;
      address->protocol = 0;
      address->address_len = sizeof(struct sockaddr_un);
      
      if (strstr(display, ":file:") == display)
	return set_af_unix(&(address->address), "%s", display + 6);
      else if (display[1] && is_pzinteger(display + 1))
	return set_af_unix(&(address->address), "%s/%s.socket",
			   MDS_RUNTIME_ROOT_DIRECTORY, display + 1);
      return 0;
    }
  
  host = alloca((strlen(display) + 1) * sizeof(char));
  
  if (*display == '[')
    colesc = 1, i++;
  for (; (c = display[i]); i++)
    if (esc)             host[pathlen++] = c, esc = 0;
    else if (c == '\\')  esc = 1;
    else if (c == ']')
      if (colesc)        { i++; break; }
      else               host[pathlen++] = c;
    else if (c == ':')
      if (colesc)        host[pathlen++] = c;
      else               break;
    else                 host[pathlen++] = c;
  if (esc || (display[i++] != ':'))
    return 0;
  host[pathlen++] = '\0';
  
  port = host + pathlen;
  memcpy(port, display + i, (strlen(display) + 1 - i) * sizeof(char));
  params = strchr(port, ':');
  if (params != NULL)
    *params++ = '\0';
  
#define param_test(f, n, a, b, c)			\
  ((n >= 3) && (!strcasecmp(params, a ":" b ":" c))) ||	\
  ((n >= 2) && (!strcasecmp(params, a ":" b))) ||	\
  ((n >= 1) && (!strcasecmp(params, a))) ||		\
  (f == NULL ? 0 : !strcasecmp(params, f))
#define set(d, t, p)  \
  address->domain = (d), address->type = (t), address->protocol = (p)
  
  if      (params == NULL)                                        set(PF_UNSPEC, SOCK_STREAM, IPPROTO_TCP);
  else if (param_test("ip/tcp",    1, "ip",    "stream", "tcp"))  set(PF_UNSPEC, SOCK_STREAM, IPPROTO_TCP);
  else if (param_test("ipv4/tcp",  1, "ipv4",  "stream", "tcp"))  set(PF_INET,   SOCK_STREAM, IPPROTO_TCP);
  else if (param_test("ipv6/tcp",  1, "ipv6",  "stream", "tcp"))  set(PF_INET6,  SOCK_STREAM, IPPROTO_TCP);
  else if (param_test("inet/tcp",  1, "inet",  "stream", "tcp"))  set(PF_INET,   SOCK_STREAM, IPPROTO_TCP);
  else if (param_test("inet6/tcp", 1, "inet6", "stream", "tcp"))  set(PF_INET6,  SOCK_STREAM, IPPROTO_TCP);
  else
    return 0;
  
#undef set
#undef param_test
  
  return set_af_inet(&(address->address), &(address->address_len),
		     &(address->gai_error), &(address->domain),
		     address->domain == PF_UNSPEC ? AF_UNSPEC :
		     address->domain == PF_INET   ? AF_INET   :
		     address->domain == PF_INET6  ? AF_INET6  :
		     address->domain, host, port);
}

